/*
 * $Header:
 * /home/jerenkrantz/tmp/commons/commons-convert/cvs/home/cvs/jakarta-commons
 * //httpclient/src/java/org/apache/commons/httpclient/HttpConnection.java,v
 * 1.107 2005/01/14 21:30:59 olegk Exp $ $Revision: 480424 $ $Date: 2006-11-29
 * 06:56:49 +0100 (Wed, 29 Nov 2006) $
 *
 * ====================================================================
 *
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements. See the NOTICE file distributed with this
 * work for additional information regarding copyright ownership. The ASF
 * licenses this file to You under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 * License for the specific language governing permissions and limitations under
 * the License.
 * ====================================================================
 *
 * This software consists of voluntary contributions made by many individuals on
 * behalf of the Apache Software Foundation. For more information on the Apache
 * Software Foundation, please see <http://www.apache.org/>.
 */

package com.microsoft.tfs.core.httpclient;

import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InterruptedIOException;
import java.io.OutputStream;
import java.net.InetAddress;
import java.net.Socket;
import java.net.SocketException;

import javax.net.ssl.SSLSocket;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import com.microsoft.tfs.core.httpclient.params.HttpConnectionParams;
import com.microsoft.tfs.core.httpclient.protocol.Protocol;
import com.microsoft.tfs.core.httpclient.protocol.ProtocolSocketFactory;
import com.microsoft.tfs.core.httpclient.protocol.SecureProtocolSocketFactory;
import com.microsoft.tfs.core.httpclient.util.EncodingUtil;
import com.microsoft.tfs.core.httpclient.util.ExceptionUtil;

/**
 * An abstraction of an HTTP {@link InputStream} and {@link OutputStream} pair,
 * together with the relevant attributes.
 * <p>
 * The following options are set on the socket before getting the input/output
 * streams in the {@link #open()} method:
 * <table border=1>
 * <tr>
 * <th>Socket Method
 * <th>Sockets Option
 * <th>Configuration
 * </tr>
 * <tr>
 * <td>{@link java.net.Socket#setTcpNoDelay(boolean)}
 * <td>SO_NODELAY
 * <td>{@link HttpConnectionParams#setTcpNoDelay(boolean)}
 * </tr>
 * <tr>
 * <td>{@link java.net.Socket#setSoTimeout(int)}
 * <td>SO_TIMEOUT
 * <td>{@link HttpConnectionParams#setSoTimeout(int)}
 * </tr>
 * <tr>
 * <td>{@link java.net.Socket#setSendBufferSize(int)}
 * <td>SO_SNDBUF
 * <td>{@link HttpConnectionParams#setSendBufferSize(int)}
 * </tr>
 * <tr>
 * <td>{@link java.net.Socket#setReceiveBufferSize(int)}
 * <td>SO_RCVBUF
 * <td>{@link HttpConnectionParams#setReceiveBufferSize(int)}
 * </tr>
 * </table>
 *
 * @author Rod Waldhoff
 * @author Sean C. Sullivan
 * @author Ortwin Glueck
 * @author <a href="mailto:jsdever@apache.org">Jeff Dever</a>
 * @author <a href="mailto:mbowler@GargoyleSoftware.com">Mike Bowler</a>
 * @author <a href="mailto:oleg@ural.ru">Oleg Kalnichevski</a>
 * @author Michael Becke
 * @author Eric E Johnson
 * @author Laura Werner
 *
 * @version $Revision: 480424 $ $Date: 2006-11-29 06:56:49 +0100 (Wed, 29 Nov
 *          2006) $
 */
public class HttpConnection {

    // ----------------------------------------------------------- Constructors

    /**
     * Creates a new HTTP connection for the given host and port.
     *
     * @param host
     *        the host to connect to
     * @param port
     *        the port to connect to
     */
    public HttpConnection(final String host, final int port) {
        this(null, -1, host, null, port, Protocol.getProtocol("http"));
    }

    /**
     * Creates a new HTTP connection for the given host and port using the given
     * protocol.
     *
     * @param host
     *        the host to connect to
     * @param port
     *        the port to connect to
     * @param protocol
     *        the protocol to use
     */
    public HttpConnection(final String host, final int port, final Protocol protocol) {
        this(null, -1, host, null, port, protocol);
    }

    /**
     * Creates a new HTTP connection for the given host with the virtual alias
     * and port using given protocol.
     *
     * @param host
     *        the host to connect to
     * @param virtualHost
     *        the virtual host requests will be sent to
     * @param port
     *        the port to connect to
     * @param protocol
     *        the protocol to use
     */
    public HttpConnection(final String host, final String virtualHost, final int port, final Protocol protocol) {
        this(null, -1, host, virtualHost, port, protocol);
    }

    /**
     * Creates a new HTTP connection for the given host and port via the given
     * proxy host and port using the default protocol.
     *
     * @param proxyHost
     *        the host to proxy via
     * @param proxyPort
     *        the port to proxy via
     * @param host
     *        the host to connect to
     * @param port
     *        the port to connect to
     */
    public HttpConnection(final String proxyHost, final int proxyPort, final String host, final int port) {
        this(proxyHost, proxyPort, host, null, port, Protocol.getProtocol("http"));
    }

    /**
     * Creates a new HTTP connection for the given host configuration.
     *
     * @param hostConfiguration
     *        the host/proxy/protocol to use
     */
    public HttpConnection(final HostConfiguration hostConfiguration) {
        this(
            hostConfiguration.getProxyHost(),
            hostConfiguration.getProxyPort(),
            hostConfiguration.getHost(),
            hostConfiguration.getPort(),
            hostConfiguration.getProtocol());
        localAddress = hostConfiguration.getLocalAddress();
    }

    /**
     * Creates a new HTTP connection for the given host with the virtual alias
     * and port via the given proxy host and port using the given protocol.
     *
     * @param proxyHost
     *        the host to proxy via
     * @param proxyPort
     *        the port to proxy via
     * @param host
     *        the host to connect to. Parameter value must be non-null.
     * @param virtualHost
     *        No longer applicable.
     * @param port
     *        the port to connect to
     * @param protocol
     *        The protocol to use. Parameter value must be non-null.
     *
     * @deprecated use #HttpConnection(String, int, String, int, Protocol)
     */
    @Deprecated
    public HttpConnection(
        final String proxyHost,
        final int proxyPort,
        final String host,
        final String virtualHost,
        final int port,
        final Protocol protocol) {
        this(proxyHost, proxyPort, host, port, protocol);
    }

    /**
     * Creates a new HTTP connection for the given host with the virtual alias
     * and port via the given proxy host and port using the given protocol.
     *
     * @param proxyHost
     *        the host to proxy via
     * @param proxyPort
     *        the port to proxy via
     * @param host
     *        the host to connect to. Parameter value must be non-null.
     * @param port
     *        the port to connect to
     * @param protocol
     *        The protocol to use. Parameter value must be non-null.
     */
    public HttpConnection(
        final String proxyHost,
        final int proxyPort,
        final String host,
        final int port,
        final Protocol protocol) {

        if (host == null) {
            throw new IllegalArgumentException("host parameter is null");
        }
        if (protocol == null) {
            throw new IllegalArgumentException("protocol is null");
        }

        synchronized (syncLastID) {
            ID = ++lastID;
        }

        proxyHostName = proxyHost;
        proxyPortNumber = proxyPort;
        hostName = host;
        portNumber = protocol.resolvePort(port);
        protocolInUse = protocol;
    }

    public int getID() {
        return ID;
    }

    // ------------------------------------------ Attribute Setters and Getters

    /**
     * Returns the connection socket.
     *
     * @return the socket.
     *
     * @since 3.0
     */
    public Socket getSocket() {
        return socket;
    }

    /**
     * Returns the host.
     *
     * @return the host.
     */
    public String getHost() {
        return hostName;
    }

    /**
     * Sets the host to connect to.
     *
     * @param host
     *        the host to connect to. Parameter value must be non-null.
     * @throws IllegalStateException
     *         if the connection is already open
     */
    public void setHost(final String host) throws IllegalStateException {
        if (host == null) {
            throw new IllegalArgumentException("host parameter is null");
        }
        assertNotOpen();
        hostName = host;
    }

    /**
     * Returns the target virtual host.
     *
     * @return the virtual host.
     *
     * @deprecated no longer applicable
     */

    @Deprecated
    public String getVirtualHost() {
        return hostName;
    }

    /**
     * Sets the virtual host to target.
     *
     * @param host
     *        the virtual host name that should be used instead of physical host
     *        name when sending HTTP requests. Virtual host name can be set to
     *        <tt> null</tt> if virtual host name is not to be used
     *
     * @throws IllegalStateException
     *         if the connection is already open
     *
     * @deprecated no longer applicable
     */

    @Deprecated
    public void setVirtualHost(final String host) throws IllegalStateException {
        assertNotOpen();
    }

    /**
     * Returns the port of the host.
     *
     * If the port is -1 (or less than 0) the default port for the current
     * protocol is returned.
     *
     * @return the port.
     */
    public int getPort() {
        if (portNumber < 0) {
            return isSecure() ? 443 : 80;
        } else {
            return portNumber;
        }
    }

    /**
     * Sets the port to connect to.
     *
     * @param port
     *        the port to connect to
     *
     * @throws IllegalStateException
     *         if the connection is already open
     */
    public void setPort(final int port) throws IllegalStateException {
        assertNotOpen();
        portNumber = port;
    }

    /**
     * Returns the proxy host.
     *
     * @return the proxy host.
     */
    public String getProxyHost() {
        return proxyHostName;
    }

    /**
     * Sets the host to proxy through.
     *
     * @param host
     *        the host to proxy through.
     *
     * @throws IllegalStateException
     *         if the connection is already open
     */
    public void setProxyHost(final String host) throws IllegalStateException {
        assertNotOpen();
        proxyHostName = host;
    }

    /**
     * Returns the port of the proxy host.
     *
     * @return the proxy port.
     */
    public int getProxyPort() {
        return proxyPortNumber;
    }

    /**
     * Sets the port of the host to proxy through.
     *
     * @param port
     *        the port of the host to proxy through.
     *
     * @throws IllegalStateException
     *         if the connection is already open
     */
    public void setProxyPort(final int port) throws IllegalStateException {
        assertNotOpen();
        proxyPortNumber = port;
    }

    /**
     * Returns <tt>true</tt> if the connection is established over a secure
     * protocol.
     *
     * @return <tt>true</tt> if connected over a secure protocol.
     */
    public boolean isSecure() {
        return protocolInUse.isSecure();
    }

    /**
     * Returns the protocol used to establish the connection.
     *
     * @return The protocol
     */
    public Protocol getProtocol() {
        return protocolInUse;
    }

    /**
     * Sets the protocol used to establish the connection
     *
     * @param protocol
     *        The protocol to use.
     *
     * @throws IllegalStateException
     *         if the connection is already open
     */
    public void setProtocol(final Protocol protocol) {
        assertNotOpen();

        if (protocol == null) {
            throw new IllegalArgumentException("protocol is null");
        }

        protocolInUse = protocol;

    }

    /**
     * Return the local address used when creating the connection. If
     * <tt>null</tt>, the default address is used.
     *
     * @return InetAddress the local address to be used when creating Sockets
     */
    public InetAddress getLocalAddress() {
        return localAddress;
    }

    /**
     * Set the local address used when creating the connection. If unset or
     * <tt>null</tt>, the default address is used.
     *
     * @param localAddress
     *        the local address to use
     */
    public void setLocalAddress(final InetAddress localAddress) {
        assertNotOpen();
        this.localAddress = localAddress;
    }

    /**
     * Tests if the connection is open.
     *
     * @return <code>true</code> if the connection is open
     */
    public boolean isOpen() {
        return isOpen;
    }

    /**
     * Closes the connection if stale.
     *
     * @return <code>true</code> if the connection was stale and therefore
     *         closed, <code>false</code> otherwise.
     *
     * @see #isStale()
     *
     * @since 3.0
     */
    public boolean closeIfStale() throws IOException {
        if (isOpen && isStale()) {
            LOG.debug("Connection is stale, closing...");
            close();
            return true;
        }
        return false;
    }

    /**
     * Tests if stale checking is enabled.
     *
     * @return <code>true</code> if enabled
     *
     * @see #isStale()
     *
     * @deprecated Use {@link HttpConnectionParams#isStaleCheckingEnabled()},
     *             {@link HttpConnection#getParams()}.
     */
    @Deprecated
    public boolean isStaleCheckingEnabled() {
        return params.isStaleCheckingEnabled();
    }

    /**
     * Sets whether or not isStale() will be called when testing if this
     * connection is open.
     *
     * <p>
     * Setting this flag to <code>false</code> will increase performance when
     * reusing connections, but it will also make them less reliable. Stale
     * checking ensures that connections are viable before they are used. When
     * set to <code>false</code> some method executions will result in
     * IOExceptions and they will have to be retried.
     * </p>
     *
     * @param staleCheckEnabled
     *        <code>true</code> to enable isStale()
     *
     * @see #isStale()
     * @see #isOpen()
     *
     * @deprecated Use
     *             {@link HttpConnectionParams#setStaleCheckingEnabled(boolean)}
     *             , {@link HttpConnection#getParams()}.
     */
    @Deprecated
    public void setStaleCheckingEnabled(final boolean staleCheckEnabled) {
        params.setStaleCheckingEnabled(staleCheckEnabled);
    }

    /**
     * Determines whether this connection is "stale", which is to say that
     * either it is no longer open, or an attempt to read the connection would
     * fail.
     *
     * <p>
     * Unfortunately, due to the limitations of the JREs prior to 1.4, it is not
     * possible to test a connection to see if both the read and write channels
     * are open - except by reading and writing. This leads to a difficulty when
     * some connections leave the "write" channel open, but close the read
     * channel and ignore the request. This function attempts to ameliorate that
     * problem by doing a test read, assuming that the caller will be doing a
     * write followed by a read, rather than the other way around.
     * </p>
     *
     * <p>
     * To avoid side-effects, the underlying connection is wrapped by a
     * {@link BufferedInputStream}, so although data might be read, what is
     * visible to clients of the connection will not change with this call. </p
     * .
     *
     * @throws IOException
     *         if the stale connection test is interrupted.
     *
     * @return <tt>true</tt> if the connection is already closed, or a read
     *         would fail.
     */
    protected boolean isStale() throws IOException {
        LOG.trace("enter HttpConnection.isStale()");
        boolean isStale = true;
        if (isOpen) {
            // the connection is open, but now we have to see if we can read it
            // assume the connection is not stale.
            isStale = false;
            try {
                if (inputStream.available() <= 0) {
                    try {
                        socket.setSoTimeout(1);
                        inputStream.mark(1);
                        final int byteRead = inputStream.read();
                        if (byteRead == -1) {
                            // again - if the socket is reporting all data read,
                            // probably stale
                            isStale = true;
                        } else {
                            inputStream.reset();
                        }
                    } finally {
                        socket.setSoTimeout(params.getSoTimeout());
                    }
                }
            } catch (final InterruptedIOException e) {
                if (!ExceptionUtil.isSocketTimeoutException(e)) {
                    LOG.trace("Unexpected InterruptedIOException in isStale method ", e);
                    throw e;
                }
                // aha - the connection is NOT stale - continue on!
            } catch (final Throwable e) {
                // oops - the connection is stale, the read or soTimeout failed.
                LOG.debug("An error occurred while reading from the socket, is appears to be stale", e);
                isStale = true;
            }
        }

        return isStale;
    }

    /**
     * Returns <tt>true</tt> if the connection is established via a proxy,
     * <tt>false</tt> otherwise.
     *
     * @return <tt>true</tt> if a proxy is used to establish the connection,
     *         <tt>false</tt> otherwise.
     */
    public boolean isProxied() {
        return (!(null == proxyHostName || 0 >= proxyPortNumber));
    }

    /**
     * Set the state to keep track of the last response for the last request.
     *
     * <p>
     * The connection managers use this to ensure that previous requests are
     * properly closed before a new request is attempted. That way, a GET
     * request need not be read in its entirety before a new request is issued.
     * Instead, this stream can be closed as appropriate.
     * </p>
     *
     * @param inStream
     *        The stream associated with an HttpMethod.
     */
    public void setLastResponseInputStream(final InputStream inStream) {
        lastResponseInputStream = inStream;
    }

    /**
     * Returns the stream used to read the last response's body.
     *
     * <p>
     * Clients will generally not need to call this function unless using
     * HttpConnection directly, instead of calling
     * {@link HttpClient#executeMethod}. For those clients, call this function,
     * and if it returns a non-null stream, close the stream before attempting
     * to execute a method. Note that calling "close" on the stream returned by
     * this function <i>may</i> close the connection if the previous response
     * contained a "Connection: close" header.
     * </p>
     *
     * @return An {@link InputStream} corresponding to the body of the last
     *         response.
     */
    public InputStream getLastResponseInputStream() {
        return lastResponseInputStream;
    }

    // --------------------------------------------------- Other Public Methods

    /**
     * Returns {@link HttpConnectionParams HTTP protocol parameters} associated
     * with this method.
     *
     * @return HTTP parameters.
     *
     * @since 3.0
     */
    public HttpConnectionParams getParams() {
        return params;
    }

    /**
     * Assigns {@link HttpConnectionParams HTTP protocol parameters} for this
     * method.
     *
     * @since 3.0
     *
     * @see HttpConnectionParams
     */
    public void setParams(final HttpConnectionParams params) {
        if (params == null) {
            throw new IllegalArgumentException("Parameters may not be null");
        }
        this.params = params;
    }

    /**
     * Set the {@link Socket}'s timeout, via {@link Socket#setSoTimeout}. If the
     * connection is already open, the SO_TIMEOUT is changed. If no connection
     * is open, then subsequent connections will use the timeout value.
     * <p>
     * Note: This is not a connection timeout but a timeout on network traffic!
     *
     * @param timeout
     *        the timeout value
     * @throws SocketException
     *         - if there is an error in the underlying protocol, such as a TCP
     *         error.
     *
     * @deprecated Use {@link HttpConnectionParams#setSoTimeout(int)},
     *             {@link HttpConnection#getParams()}.
     */
    @Deprecated
    public void setSoTimeout(final int timeout) throws SocketException, IllegalStateException {
        params.setSoTimeout(timeout);
        if (socket != null) {
            LOG.trace("Set socket timeout to " + timeout);
            try {
                socket.setSoTimeout(timeout);
            } catch (final Throwable t) {
                LOG.debug("", t);
                if (t instanceof SocketException) {
                    throw (SocketException) t;
                }
                if (t instanceof IllegalStateException) {
                    throw (IllegalStateException) t;
                }
                throw new Error(t);
            }
        }
    }

    /**
     * Sets <code>SO_TIMEOUT</code> value directly on the underlying
     * {@link Socket socket}. This method does not change the default read
     * timeout value set via {@link HttpConnectionParams}.
     *
     * @param timeout
     *        the timeout value
     * @throws SocketException
     *         - if there is an error in the underlying protocol, such as a TCP
     *         error.
     * @throws IllegalStateException
     *         if not connected
     *
     * @since 3.0
     */
    public void setSocketTimeout(final int timeout) throws SocketException, IllegalStateException {
        assertOpen();
        if (socket != null) {
            LOG.trace("Set socket timeout to " + timeout);
            try {
                socket.setSoTimeout(timeout);
            } catch (final Throwable t) {
                LOG.debug("", t);
                if (t instanceof SocketException) {
                    throw (SocketException) t;
                }
                if (t instanceof IllegalStateException) {
                    throw (IllegalStateException) t;
                }
                throw new SocketException(t.getMessage());
            }
        }
    }

    /**
     * Returns the {@link Socket}'s timeout, via {@link Socket#getSoTimeout}, if
     * the connection is already open. If no connection is open, return the
     * value subsequent connection will use.
     * <p>
     * Note: This is not a connection timeout but a timeout on network traffic!
     *
     * @return the timeout value
     *
     * @deprecated Use {@link HttpConnectionParams#getSoTimeout()},
     *             {@link HttpConnection#getParams()}.
     */
    @Deprecated
    public int getSoTimeout() throws SocketException {
        return params.getSoTimeout();
    }

    /**
     * Sets the connection timeout. This is the maximum time that may be spent
     * until a connection is established. The connection will fail after this
     * amount of time.
     *
     * @param timeout
     *        The timeout in milliseconds. 0 means timeout is not used.
     *
     * @deprecated Use {@link HttpConnectionParams#setConnectionTimeout(int)},
     *             {@link HttpConnection#getParams()}.
     */
    @Deprecated
    public void setConnectionTimeout(final int timeout) {
        params.setConnectionTimeout(timeout);
    }

    /**
     * Establishes a connection to the specified host and port (via a proxy if
     * specified). The underlying socket is created from the
     * {@link ProtocolSocketFactory}.
     *
     * @throws IOException
     *         if an attempt to establish the connection results in an I/O
     *         error.
     */
    public void open() throws IOException {
        LOG.trace("enter HttpConnection.open()");

        final String host = (proxyHostName == null) ? hostName : proxyHostName;
        final int port = (proxyHostName == null) ? portNumber : proxyPortNumber;
        assertNotOpen();

        if (LOG.isDebugEnabled()) {
            LOG.debug("Open connection to " + host + ":" + port);
        }

        try {
            if (socket == null) {
                usingSecureSocket = isSecure() && !isProxied();
                // use the protocol's socket factory unless this is a secure
                // proxied connection
                ProtocolSocketFactory socketFactory = null;
                if (isSecure() && isProxied()) {
                    final Protocol defaultprotocol = Protocol.getProtocol("http");
                    socketFactory = defaultprotocol.getSocketFactory();
                } else {
                    socketFactory = protocolInUse.getSocketFactory();
                }
                socket = socketFactory.createSocket(host, port, localAddress, 0, params);
            }

            /*
             * "Nagling has been broadly implemented across networks, including
             * the Internet, and is generally performed by default - although it
             * is sometimes considered to be undesirable in highly interactive
             * environments, such as some client/server situations. In such
             * cases, nagling may be turned off through use of the TCP_NODELAY
             * sockets option."
             */

            socket.setTcpNoDelay(params.getTcpNoDelay());
            socket.setSoTimeout(params.getSoTimeout());

            final int linger = params.getLinger();
            if (linger >= 0) {
                socket.setSoLinger(linger > 0, linger);
            }

            final int sndBufSize = params.getSendBufferSize();
            if (sndBufSize >= 0) {
                socket.setSendBufferSize(sndBufSize);
            }
            final int rcvBufSize = params.getReceiveBufferSize();
            if (rcvBufSize >= 0) {
                socket.setReceiveBufferSize(rcvBufSize);
            }
            int outbuffersize = socket.getSendBufferSize();
            if ((outbuffersize > 2048) || (outbuffersize <= 0)) {
                outbuffersize = 2048;
            }
            int inbuffersize = socket.getReceiveBufferSize();
            if ((inbuffersize > 2048) || (inbuffersize <= 0)) {
                inbuffersize = 2048;
            }
            inputStream = new BufferedInputStream(socket.getInputStream(), inbuffersize);
            outputStream = new BufferedOutputStream(socket.getOutputStream(), outbuffersize);
            isOpen = true;
        } catch (final IOException e) {
            LOG.debug("", e);
            // Connection wasn't opened properly
            // so close everything out
            closeSocketAndStreams();
            throw e;
        }
    }

    /**
     * Instructs the proxy to establish a secure tunnel to the host. The socket
     * will be switched to the secure socket. Subsequent communication is done
     * via the secure socket. The method can only be called once on a proxied
     * secure connection.
     *
     * @throws IllegalStateException
     *         if connection is not secure and proxied or if the socket is
     *         already secure.
     * @throws IOException
     *         if an attempt to establish the secure tunnel results in an I/O
     *         error.
     */
    public void tunnelCreated() throws IllegalStateException, IOException {
        LOG.trace("enter HttpConnection.tunnelCreated()");

        if (!isSecure() || !isProxied()) {
            throw new IllegalStateException("Connection must be secure " + "and proxied to use this feature");
        }

        if (usingSecureSocket) {
            throw new IllegalStateException("Already using a secure socket");
        }

        if (LOG.isDebugEnabled()) {
            LOG.debug("Secure tunnel to " + hostName + ":" + portNumber);
        }

        final SecureProtocolSocketFactory socketFactory =
            (SecureProtocolSocketFactory) protocolInUse.getSocketFactory();

        socket = socketFactory.createSocket(socket, hostName, portNumber, params, true);
        final int sndBufSize = params.getSendBufferSize();
        if (sndBufSize >= 0) {
            socket.setSendBufferSize(sndBufSize);
        }
        final int rcvBufSize = params.getReceiveBufferSize();
        if (rcvBufSize >= 0) {
            socket.setReceiveBufferSize(rcvBufSize);
        }
        int outbuffersize = socket.getSendBufferSize();
        if (outbuffersize > 2048) {
            outbuffersize = 2048;
        }
        int inbuffersize = socket.getReceiveBufferSize();
        if (inbuffersize > 2048) {
            inbuffersize = 2048;
        }
        inputStream = new BufferedInputStream(socket.getInputStream(), inbuffersize);
        outputStream = new BufferedOutputStream(socket.getOutputStream(), outbuffersize);
        usingSecureSocket = true;
        tunnelEstablished = true;
    }

    /**
     * Indicates if the connection is completely transparent from end to end.
     *
     * @return true if conncetion is not proxied or tunneled through a
     *         transparent proxy; false otherwise.
     */
    public boolean isTransparent() {
        return !isProxied() || tunnelEstablished;
    }

    /**
     * Flushes the output request stream. This method should be called to ensure
     * that data written to the request OutputStream is sent to the server.
     *
     * @throws IOException
     *         if an I/O problem occurs
     */
    public void flushRequestOutputStream() throws IOException {
        LOG.trace("enter HttpConnection.flushRequestOutputStream()");
        assertOpen();
        outputStream.flush();
    }

    /**
     * Returns an {@link OutputStream} suitable for writing the request.
     *
     * @throws IllegalStateException
     *         if the connection is not open
     * @throws IOException
     *         if an I/O problem occurs
     * @return a stream to write the request to
     */
    public OutputStream getRequestOutputStream() throws IOException, IllegalStateException {
        LOG.trace("enter HttpConnection.getRequestOutputStream()");
        assertOpen();
        OutputStream out = outputStream;
        if (Wire.CONTENT_WIRE.enabled()) {
            out = new WireLogOutputStream(out, Wire.CONTENT_WIRE);
        }
        return out;
    }

    /**
     * Return a {@link InputStream} suitable for reading the response.
     *
     * @return InputStream The response input stream.
     * @throws IOException
     *         If an IO problem occurs
     * @throws IllegalStateException
     *         If the connection isn't open.
     */
    public InputStream getResponseInputStream() throws IOException, IllegalStateException {
        LOG.trace("enter HttpConnection.getResponseInputStream()");
        assertOpen();
        return inputStream;
    }

    /**
     * Tests if input data avaialble. This method returns immediately and does
     * not perform any read operations on the input socket
     *
     * @return boolean <tt>true</tt> if input data is available, <tt>false</tt>
     *         otherwise.
     *
     * @throws IOException
     *         If an IO problem occurs
     * @throws IllegalStateException
     *         If the connection isn't open.
     */
    public boolean isResponseAvailable() throws IOException {
        LOG.trace("enter HttpConnection.isResponseAvailable()");
        try {
            if (isOpen) {
                return inputStream.available() > 0;
            } else {
                return false;
            }
        } catch (final Throwable t) {
            LOG.debug("", t);
            if (t instanceof IOException) {
                throw (IOException) t;
            }

            throw new Error(t);
        }
    }

    /**
     * Tests if input data becomes available within the given period time in
     * milliseconds.
     *
     * @param timeout
     *        The number milliseconds to wait for input data to become available
     * @return boolean <tt>true</tt> if input data is availble, <tt>false</tt>
     *         otherwise.
     *
     * @throws IOException
     *         If an IO problem occurs
     * @throws IllegalStateException
     *         If the connection isn't open.
     */
    public boolean isResponseAvailable(final int timeout) throws IOException {
        LOG.trace("enter HttpConnection.isResponseAvailable(int timeout: " + timeout + ")");
        assertOpen();
        boolean result = false;
        if (inputStream.available() > 0) {
            result = true;
        } else {
            try {
                socket.setSoTimeout(timeout);
                inputStream.mark(1);
                final int byteRead = inputStream.read();
                if (byteRead != -1) {
                    inputStream.reset();
                    LOG.debug("Input data available");
                    result = true;
                } else {
                    LOG.debug("Input data not available");
                }
            } catch (final InterruptedIOException e) {
                if (!ExceptionUtil.isSocketTimeoutException(e)) {
                    throw e;
                }
                if (LOG.isDebugEnabled()) {
                    LOG.debug("Input data not available after " + timeout + " ms");
                }
            } finally {
                try {
                    socket.setSoTimeout(params.getSoTimeout());
                } catch (final IOException ioe) {
                    LOG.debug(
                        "An error ocurred while resetting soTimeout, we will assume that"
                            + " no response is available.",
                        ioe);
                    result = false;
                }
            }
        }
        return result;
    }

    /**
     * Writes the specified bytes to the output stream.
     *
     * @param data
     *        the data to be written
     * @throws IllegalStateException
     *         if not connected
     * @throws IOException
     *         if an I/O problem occurs
     * @see #write(byte[],int,int)
     */
    public void write(final byte[] data) throws IOException, IllegalStateException {
        LOG.trace("enter HttpConnection.write(byte[])");
        try {
            this.write(data, 0, data.length);
        } catch (final Throwable t) {
            LOG.debug("", t);
            if (t instanceof SocketException) {
                throw (SocketException) t;
            }
            if (t instanceof IllegalStateException) {
                throw (IllegalStateException) t;
            }
            throw new Error(t);
        }
    }

    /**
     * Writes <i>length</i> bytes in <i>data</i> starting at <i>offset</i> to
     * the output stream.
     *
     * The general contract for write(b, off, len) is that some of the bytes in
     * the array b are written to the output stream in order; element b[off] is
     * the first byte written and b[off+len-1] is the last byte written by this
     * operation.
     *
     * @param data
     *        array containing the data to be written.
     * @param offset
     *        the start offset in the data.
     * @param length
     *        the number of bytes to write.
     * @throws IllegalStateException
     *         if not connected
     * @throws IOException
     *         if an I/O problem occurs
     */
    public void write(final byte[] data, final int offset, final int length) throws IOException, IllegalStateException {
        LOG.trace("enter HttpConnection.write(byte[], int, int)");

        if (offset < 0) {
            throw new IllegalArgumentException("Array offset may not be negative");
        }
        if (length < 0) {
            throw new IllegalArgumentException("Array length may not be negative");
        }
        if (offset + length > data.length) {
            throw new IllegalArgumentException("Given offset and length exceed the array length");
        }
        assertOpen();
        try {
            outputStream.write(data, offset, length);
        } catch (final Throwable t) {
            LOG.debug("", t);
            if (t instanceof IOException) {
                throw (IOException) t;
            }
            if (t instanceof IllegalStateException) {
                throw (IllegalStateException) t;
            }
            throw new Error(t);
        }
    }

    /**
     * Writes the specified bytes, followed by <tt>"\r\n".getBytes()</tt> to the
     * output stream.
     *
     * @param data
     *        the bytes to be written
     * @throws IllegalStateException
     *         if the connection is not open
     * @throws IOException
     *         if an I/O problem occurs
     */
    public void writeLine(final byte[] data) throws IOException, IllegalStateException {
        LOG.trace("enter HttpConnection.writeLine(byte[])");
        write(data);
        writeLine();
    }

    /**
     * Writes <tt>"\r\n".getBytes()</tt> to the output stream.
     *
     * @throws IllegalStateException
     *         if the connection is not open
     * @throws IOException
     *         if an I/O problem occurs
     */
    public void writeLine() throws IOException, IllegalStateException {
        LOG.trace("enter HttpConnection.writeLine()");
        write(CRLF);
    }

    /**
     * @deprecated Use {@link #print(String, String)}
     *
     *             Writes the specified String (as bytes) to the output stream.
     *
     * @param data
     *        the string to be written
     * @throws IllegalStateException
     *         if the connection is not open
     * @throws IOException
     *         if an I/O problem occurs
     */
    @Deprecated
    public void print(final String data) throws IOException, IllegalStateException {
        LOG.trace("enter HttpConnection.print(String)");
        write(EncodingUtil.getBytes(data, "ISO-8859-1"));
    }

    /**
     * Writes the specified String (as bytes) to the output stream.
     *
     * @param data
     *        the string to be written
     * @param charset
     *        the charset to use for writing the data
     * @throws IllegalStateException
     *         if the connection is not open
     * @throws IOException
     *         if an I/O problem occurs
     *
     * @since 3.0
     */
    public void print(final String data, final String charset) throws IOException, IllegalStateException {
        LOG.trace("enter HttpConnection.print(String)");
        write(EncodingUtil.getBytes(data, charset));
    }

    /**
     * @deprecated Use {@link #printLine(String, String)}
     *
     *             Writes the specified String (as bytes), followed by
     *             <tt>"\r\n".getBytes()</tt> to the output stream.
     *
     * @param data
     *        the data to be written
     * @throws IllegalStateException
     *         if the connection is not open
     * @throws IOException
     *         if an I/O problem occurs
     */
    @Deprecated
    public void printLine(final String data) throws IOException, IllegalStateException {
        LOG.trace("enter HttpConnection.printLine(String)");
        writeLine(EncodingUtil.getBytes(data, "ISO-8859-1"));
    }

    /**
     * Writes the specified String (as bytes), followed by
     * <tt>"\r\n".getBytes()</tt> to the output stream.
     *
     * @param data
     *        the data to be written
     * @param charset
     *        the charset to use for writing the data
     * @throws IllegalStateException
     *         if the connection is not open
     * @throws IOException
     *         if an I/O problem occurs
     *
     * @since 3.0
     */
    public void printLine(final String data, final String charset) throws IOException, IllegalStateException {
        LOG.trace("enter HttpConnection.printLine(String)");
        writeLine(EncodingUtil.getBytes(data, charset));
    }

    /**
     * Writes <tt>"\r\n".getBytes()</tt> to the output stream.
     *
     * @throws IllegalStateException
     *         if the connection is not open
     * @throws IOException
     *         if an I/O problem occurs
     */
    public void printLine() throws IOException, IllegalStateException {
        LOG.trace("enter HttpConnection.printLine()");
        writeLine();
    }

    /**
     * Reads up to <tt>"\n"</tt> from the (unchunked) input stream. If the
     * stream ends before the line terminator is found, the last part of the
     * string will still be returned.
     *
     * @throws IllegalStateException
     *         if the connection is not open
     * @throws IOException
     *         if an I/O problem occurs
     * @return a line from the response
     *
     * @deprecated use #readLine(String)
     */
    @Deprecated
    public String readLine() throws IOException, IllegalStateException {
        LOG.trace("enter HttpConnection.readLine()");

        assertOpen();
        try {
            return HttpParser.readLine(inputStream);
        } catch (final Throwable t) {
            LOG.debug("", t);
            if (t instanceof IOException) {
                throw (IOException) t;
            }
            if (t instanceof IllegalStateException) {
                throw (IllegalStateException) t;
            }
            throw new Error(t);
        }
    }

    /**
     * Reads up to <tt>"\n"</tt> from the (unchunked) input stream. If the
     * stream ends before the line terminator is found, the last part of the
     * string will still be returned.
     *
     * @param charset
     *        the charset to use for reading the data
     *
     * @throws IllegalStateException
     *         if the connection is not open
     * @throws IOException
     *         if an I/O problem occurs
     * @return a line from the response
     *
     * @since 3.0
     */
    public String readLine(final String charset) throws IOException, IllegalStateException {
        LOG.trace("enter HttpConnection.readLine()");

        assertOpen();
        try {
            return HttpParser.readLine(inputStream, charset);
        } catch (final Throwable t) {
            LOG.debug("", t);
            if (t instanceof IOException) {
                throw (IOException) t;
            }
            if (t instanceof IllegalStateException) {
                throw (IllegalStateException) t;
            }
            throw new Error(t);
        }
    }

    /**
     * Attempts to shutdown the {@link Socket}'s output, via
     * Socket.shutdownOutput() when running on JVM 1.3 or higher.
     *
     * @deprecated unused
     */
    @Deprecated
    public void shutdownOutput() {
        LOG.trace("enter HttpConnection.shutdownOutput()");

        if (socket != null && !(socket instanceof SSLSocket)) {
            try {
                socket.shutdownOutput();
            } catch (final Exception ex) {
                LOG.debug("Unexpected Exception caught", ex);
                // Ignore, and hope everything goes right
            }
        }
        // close output stream?
    }

    /**
     * Closes the socket and streams.
     */
    public void close() {
        LOG.trace("enter HttpConnection.close()");
        closeSocketAndStreams();
    }

    /**
     * Returns the httpConnectionManager.
     *
     * @return HttpConnectionManager
     */
    public HttpConnectionManager getHttpConnectionManager() {
        return httpConnectionManager;
    }

    /**
     * Sets the httpConnectionManager.
     *
     * @param httpConnectionManager
     *        The httpConnectionManager to set
     */
    public void setHttpConnectionManager(final HttpConnectionManager httpConnectionManager) {
        this.httpConnectionManager = httpConnectionManager;
    }

    /**
     * Releases the connection. If the connection is locked or does not have a
     * connection manager associated with it, this method has no effect. Note
     * that it is completely safe to call this method multiple times.
     */
    public void releaseConnection() {
        LOG.trace("enter HttpConnection.releaseConnection()");
        if (locked) {
            LOG.debug("Connection is locked.  Call to releaseConnection() ignored.");
        } else if (httpConnectionManager != null) {
            LOG.debug("Releasing connection " + getID() + " back to connection manager.");
            httpConnectionManager.releaseConnection(this);
        } else {
            LOG.warn("HttpConnectionManager is null.  Connection cannot be released.");
        }
    }

    /**
     * Tests if the connection is locked. Locked connections cannot be released.
     * An attempt to release a locked connection will have no effect.
     *
     * @return <tt>true</tt> if the connection is locked, <tt>false</tt>
     *         otherwise.
     *
     * @since 3.0
     */
    protected boolean isLocked() {
        return locked;
    }

    /**
     * Locks or unlocks the connection. Locked connections cannot be released.
     * An attempt to release a locked connection will have no effect.
     *
     * @param locked
     *        <tt>true</tt> to lock the connection, <tt>false</tt> to unlock the
     *        connection.
     *
     * @since 3.0
     */
    protected void setLocked(final boolean locked) {
        this.locked = locked;
    }

    // ------------------------------------------------------ Protected Methods

    /**
     * Closes everything out.
     */
    protected void closeSocketAndStreams() {
        LOG.trace("enter HttpConnection.closeSockedAndStreams()");

        isOpen = false;

        // no longer care about previous responses...
        lastResponseInputStream = null;

        if (null != outputStream) {
            final OutputStream temp = outputStream;
            outputStream = null;
            try {
                temp.close();
            } catch (final Throwable ex) {
                LOG.debug("Exception caught when closing output", ex);
                // ignored
            }
        }

        if (null != inputStream) {
            final InputStream temp = inputStream;
            inputStream = null;
            try {
                temp.close();
            } catch (final Throwable ex) {
                LOG.debug("Exception caught when closing input", ex);
                // ignored
            }
        }

        if (null != socket) {
            final Socket temp = socket;
            socket = null;
            if (!temp.isClosed()) {
                try {
                    temp.close();
                } catch (final Throwable ex) {
                    LOG.debug("Exception caught when closing socket", ex);
                    // ignored
                }
            }
        }

        tunnelEstablished = false;
        usingSecureSocket = false;
    }

    /**
     * Throws an {@link IllegalStateException} if the connection is already
     * open.
     *
     * @throws IllegalStateException
     *         if connected
     */
    protected void assertNotOpen() throws IllegalStateException {
        if (isOpen) {
            throw new IllegalStateException("Connection is open");
        }
    }

    /**
     * Throws an {@link IllegalStateException} if the connection is not open.
     *
     * @throws IllegalStateException
     *         if not connected
     */
    protected void assertOpen() throws IllegalStateException {
        if (!isOpen) {
            throw new IllegalStateException("Connection is not open");
        }
    }

    /**
     * Gets the socket's sendBufferSize.
     *
     * @return the size of the buffer for the socket OutputStream, -1 if the
     *         value has not been set and the socket has not been opened
     *
     * @throws SocketException
     *         if an error occurs while getting the socket value
     *
     * @see Socket#getSendBufferSize()
     */
    public int getSendBufferSize() throws SocketException {
        if (socket == null) {
            return -1;
        } else {
            return socket.getSendBufferSize();
        }
    }

    /**
     * Sets the socket's sendBufferSize.
     *
     * @param sendBufferSize
     *        the size to set for the socket OutputStream
     *
     * @throws SocketException
     *         if an error occurs while setting the socket value
     *
     * @see Socket#setSendBufferSize(int)
     *
     * @deprecated Use {@link HttpConnectionParams#setSendBufferSize(int)},
     *             {@link HttpConnection#getParams()}.
     */
    @Deprecated
    public void setSendBufferSize(final int sendBufferSize) throws SocketException {
        params.setSendBufferSize(sendBufferSize);
    }

    // ------------------------------------------------------- Static Variable

    /** <tt>"\r\n"</tt>, as bytes. */
    private static final byte[] CRLF = new byte[] {
        (byte) 13,
        (byte) 10
    };

    /** Log object for this class. */
    private static final Log LOG = LogFactory.getLog(HttpConnection.class);

    // ----------------------------------------------------- Instance Variables

    private static int lastID = 0;
    private static Object syncLastID = new Object();

    private int ID;

    /** My host. */
    private String hostName = null;

    /** My port. */
    private int portNumber = -1;

    /** My proxy host. */
    private String proxyHostName = null;

    /** My proxy port. */
    private int proxyPortNumber = -1;

    /** My client Socket. */
    private Socket socket = null;

    /** My InputStream. */
    private InputStream inputStream = null;

    /** My OutputStream. */
    private OutputStream outputStream = null;

    /** An {@link InputStream} for the response to an individual request. */
    private InputStream lastResponseInputStream = null;

    /** Whether or not the connection is connected. */
    protected boolean isOpen = false;

    /** the protocol being used */
    private Protocol protocolInUse;

    /** Collection of HTTP parameters associated with this HTTP connection */
    private HttpConnectionParams params = new HttpConnectionParams();

    /**
     * flag to indicate if this connection can be released, if locked the
     * connection cannot be released
     */
    private boolean locked = false;

    /** Whether or not the socket is a secure one. */
    private boolean usingSecureSocket = false;

    /** Whether the connection is open via a secure tunnel or not */
    private boolean tunnelEstablished = false;

    /** the connection manager that created this connection or null */
    private HttpConnectionManager httpConnectionManager;

    /**
     * The local interface on which the connection is created, or null for the
     * default
     */
    private InetAddress localAddress;
}
